汎用 locale lib(hooks/lib/locale.ts)を追加し賛辞語彙を最初の消費者にする - #4
Merged
Conversation
…o SatisfactionCapture
Generalizes work-strings.ts's design contract (LifeOS#1695: key-level
fallback, fail-open, paiUserDir()-resolved locale files) into a reusable
lib any surface can adopt, per USER/FORK/GOALS.md's stated plan for
i18n goal 2 ("Work System の hooks/lib/work-strings.ts の設計契約...を
汎用 locale lib に一般化する PR を最初に出す").
Two lookup shapes, because prose and vocabulary need opposite fallback
semantics — this is the actual design contribution here, not just a
rename of work-strings' machinery:
- `t(lang, key, fallback, vars)` — OVERRIDE. A locale bundle replaces
the caller's default for a key it translates. No baked-in EN
dictionary (unlike work-strings.ts, which legitimately owns one
scoped to Work System issue bodies) — the caller supplies its own
default, since a generic lib has no single key-space to own.
- `tList(lang, key)` — UNION. A locale bundle's array values are ADDED
to the caller's own list, never replacing it. A Japanese locale
should still recognize "perfect" as praise, not lose English
vocabulary by opting in — this is what makes "locale unset → output
unchanged" provable by construction rather than by convention: with
no bundle, tList() returns [] and the caller's list is untouched.
resolveLang() priority: explicit arg > LIFEOS_CONFIG.toml
[principal].language (new optional field, LifeosConfig.ts) >
WORK.ISSUE_LANGUAGE (read independently, not by importing
work-config.ts's unexported loadIssueLanguage() — this keeps
locale.ts standalone rather than reaching into Work System internals
for one value) > "en". Every step is try/catch-guarded and never
throws, same fail-open contract work-strings.ts uses for locale
bundles.
First consumer: SatisfactionCapture.hook.ts's positive-praise fast-path.
Its POSITIVE_PRAISE_WORDS/POSITIVE_PHRASES stay exactly as they are —
EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASES union in tList(lang,
"praise.words"/"praise.phrases") only when non-empty, so with no locale
configured they're the literal same Set object, not a copy. Also widens
the fast-path's punctuation-stripping regex to full-width CJK
punctuation (。、!?…「」) alongside the existing ASCII set — English
prompts never contain these characters, so this is a no-op for them;
it's what lets "完璧!" normalize to "完璧" and match the locale
vocabulary.
Every deletion in this diff is either a regex line replaced by a wider
version of itself (same normalization purpose) or a Set-reference read
that now points at a value which, absent locale configuration, IS the
original Set (not a copy) — same "dispatch line branching to the
original" pattern used across this fork's other PRs, applied to a value
reference instead of a function call. LifeosConfig.ts's changes are
purely additive (one optional interface field, one line threading it
through validateAndNormalize) — zero deletions.
Verified with an isolated LIFEOS_CONFIG_PATH/LIFEOS_DIR probe harness
(same style as the ASCII-guard fix):
(a) locale unset (no [principal].language, no WORK/config.yaml, no
locale file) — perfect/great job/nice/now do X/8 produce byte-
identical output to pre-change; Japanese praise ("完璧", "ありがとう")
correctly does NOT fire (no vocabulary configured, matches
English-only baseline)
(b) [principal].language = "ja" + USER/CONFIG/locales/ja.json with
praise.words/praise.phrases — English praise still fires
(union, not replacement); "完璧!", "ありがとう", "助かった" now fire
rating 8; non-praise Japanese ("次はどうする", "これを直して")
correctly produces no rating
(c) WORK.ISSUE_LANGUAGE: ja (config.yaml) with no [principal].language
set — confirms the fallback chain's third tier resolves and
activates the same vocabulary independently of the toml field
(d) malformed locale JSON — no throw, exit 0, English behavior
preserved (bundle load fails closed, tList() returns [])
Note: "2はあとで" still misreads as rating 2 on this branch in isolation
— that's the bug fix/satisfaction-rating-ascii-guard (danielmiessler#1703) already
closed, on a branch cut from `develop` after that fix landed there; this
branch was cut from `main`, which doesn't have it yet. No conflict
expected on merge — the two changes touch disjoint regions of the file
(explicit-rating guards vs. praise-vocabulary lookup).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何をするか
hooks/lib/work-strings.ts(PR danielmiessler#1695)の設計契約(キー単位フォールバック、fail-open、paiUserDir()経由の locale パス)を汎用 lib に一般化する。USER/FORK/GOALS.mdで最初に出すと決めていた PR。設計
2つの lookup 形を用意した — 散文と語彙集合で正しいフォールバック意味論が逆だから。
t(lang, key, fallback, vars)— OVERRIDE。locale が翻訳を持つキーは呼び出し元の default を上書きする。汎用 lib なので固定 EN 辞書は持たない(work-strings.ts は Work System issue 本文専用のキー空間を正当に所有しているので別)tList(lang, key)— UNION。locale の配列値は呼び出し元自身のリストに足す、置き換えない。日本語 locale を使っても英語話者の "perfect" は賛辞のままであるべき。これにより「locale 未設定 → 出力不変」が規約ではなく構造的に証明できる — バンドルが無ければtList()は[]を返し、呼び出し元のリストは触られないresolveLang()の優先順位: 明示引数 >LIFEOS_CONFIG.tomlの新設[principal].language(optional) >WORK.ISSUE_LANGUAGE(work-config.tsの非 exportloadIssueLanguage()には依存せず独立実装 — lib を Work System 内部に依存させないため) >"en"。全段階 try/catch でガードし、絶対に例外を投げない。最初の消費者
SatisfactionCapture.hook.tsの賛辞 fast-path。POSITIVE_PRAISE_WORDS/POSITIVE_PHRASESはそのまま維持し、EFFECTIVE_PRAISE_WORDS/EFFECTIVE_PRAISE_PHRASESがtList(lang, "praise.words"/"praise.phrases")が非空のときだけ union する。locale 未設定なら同一 Set オブジェクト参照(コピーですらない)。あわせて正規化の句読点除去に全角 CJK 句読点(。、!?…「」)を追加 — 英語プロンプトにはこれらの文字が現れないので no-op、「完璧!」が「完璧」に正規化されて一致するようになる。非破壊の根拠
削除行は全て「同じ正規化目的の、より広い版に置き換わった正規表現行」か「locale 未設定なら元の Set そのものを指す参照に変わった行」のいずれか。
LifeosConfig.tsの変更は完全に追加のみ(optional フィールド1個 + それを通す1行)で削除行ゼロ。検証(隔離環境の
LIFEOS_CONFIG_PATH/LIFEOS_DIRprobe harness、PR danielmiessler#1703 と同スタイル)perfect/great job/nice/now do X/8が変更前と完全一致。日本語賛辞(完璧/ありがとう)は正しく発火しない(語彙未設定=英語のみのベースラインと一致)[principal].language = "ja"+ja.jsonに praise.words/phrases: 英語賛辞は引き続き発火(union であって置換ではない)。完璧!/ありがとう/助かったが新たに rating 8 で発火。非賛辞の日本語(次はどうする/これを直して)は正しく発火しないWORK.ISSUE_LANGUAGE: ja([principal].language未設定): フォールバック連鎖の3段目が独立に機能し、同じ語彙が有効になることを確認tList()が[]を返す)なお、このブランチを単独で見ると
2はあとでが今も rating 2 に誤検出されるが、これは PR danielmiessler#1703(すでにdevelopにマージ済み)で直したバグで、本ブランチはmainから切ったため含まれていないだけ。触っている箇所が完全に別区画(レーティングガード vs 賛辞語彙参照)なのでマージ時のコンフリクトは想定していない。🤖 Generated with Claude Code